// Conic Launcher
// Copyright 2022-2026 Broken-Deer and contributors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-only

use std::{
    io::BufRead,
    process::{Command, Stdio},
    str::FromStr,
    sync::{
        Arc, Mutex,
        atomic::{AtomicBool, Ordering},
    },
    thread,
    time::{Duration, Instant},
};

use account::check_and_refresh_account;
use arguments::generate_command_arguments;
use complete::complete_files;
use config::Config;
use download::task::Progress;
use folder::{DATA_LOCATION, MinecraftLocation};
use futures::future::{AbortHandle, Abortable};
use instance::Instance;
use log::{error, info, trace};
use options::LaunchOptions;
use platform::{OsFamily, PLATFORM_INFO};
use serde::Serialize;
use tauri::{
    Runtime, State, command,
    ipc::Channel,
    plugin::{Builder, TauriPlugin},
};
use uuid::Uuid;
use version::{Version, resolve_version};

mod arguments;
mod complete;
pub mod error;
mod options;

use error::*;

#[derive(Clone, Default)]
struct PluginState {
    current_task: Arc<Mutex<Option<AbortHandle>>>,
}

pub fn init<R: Runtime>() -> TauriPlugin<R> {
    Builder::new("launch")
        .invoke_handler(tauri::generate_handler![
            cmd_create_launch_task,
            cmd_cancel_launch_task
        ])
        .build()
}

#[derive(Clone, Serialize, PartialEq)]
#[serde(tag = "job", content = "progress")]
pub enum LaunchEvent {
    Prepare,
    RefreshAccount,
    InstallAuthlibInjector(Progress),
    CompleteFiles(Progress),
    GenerateScriptlet,
    WaitForLaunch,
    LogSettingUser,
    LogLwjglVersion,
    LogOpenALLoaded,
    LogTextureLoaded,
}

#[command]
async fn cmd_create_launch_task(
    state: State<'_, PluginState>,
    config: Config,
    instance: Instance,
    channel: Channel<LaunchEvent>,
) -> Result<u32> {
    if state.current_task.lock().expect("Internal error").is_some() {
        return Err(Error::AlreadyInLaunching);
    }
    let status = Arc::new(Mutex::new(LaunchEvent::Prepare));
    let (handle, reg) = AbortHandle::new_pair();
    let future = Abortable::new(launch(config, instance, status.clone()), reg);
    {
        let mut current_task = state.current_task.lock().expect("Internal error");
        *current_task = Some(handle);
    }
    let finished = Arc::new(AtomicBool::new(false));
    let event_sender_thread = {
        let status_cloned = status.clone();
        let finished = finished.clone();
        thread::spawn(move || {
            while !finished.load(Ordering::SeqCst) {
                let _ = channel.send(status_cloned.lock().expect("Internal error").clone());
                std::thread::sleep(Duration::from_millis(100));
            }
        })
    };
    let result = match future.await {
        Ok(result) => result,
        Err(e) => Err(Error::Aborted(e)),
    };
    finished.store(true, Ordering::SeqCst);
    let _ = event_sender_thread.join();
    result
}

#[command]
async fn cmd_cancel_launch_task(state: State<'_, PluginState>) -> Result<()> {
    let mut current_task = state.current_task.lock().expect("Internal error");
    if let Some(handle) = current_task.clone() {
        handle.abort();
    }
    *current_task = None;
    Ok(())
}

/// Represents a log message associated with a specific instance.
#[derive(Clone, Serialize)]
pub struct Log {
    /// The UUID of the instance this log belongs to.
    #[serde(rename = "instanceName")]
    pub instance_id: Uuid,

    /// The content of the log message.
    pub content: String,
}

/// Launches a Minecraft instance asynchronously via the Tauri command system.
///
/// # Arguments
/// * `storage` - Application state that holds shared configuration and data.
/// * `instance` - The Minecraft instance to launch.
///
/// # Returns
/// * `Ok(())` - If the instance was successfully launched.
/// * `Err(())` - If there was an error during launch (e.g., account not found).
///
/// # Side Effects
/// * Refreshes the selected account if configured to do so.
/// * Optionally checks files before launch.
/// * Spawns the Minecraft process and generates launch script.
pub async fn launch(
    config: Config,
    instance: Instance,
    status: Arc<Mutex<LaunchEvent>>,
) -> Result<u32> {
    info!(
        "Starting Minecraft client, instance: {}",
        instance.config.name
    );
    print_instance_info(&instance);
    let minecraft_location = MinecraftLocation::new(&DATA_LOCATION.root);

    if !config.launch.skip_refresh_account {
        {
            let mut status = status.lock().expect("Internal error");
            *status = LaunchEvent::RefreshAccount;
        }
        check_and_refresh_account(config.current_account_uuid, &config.current_account_type)
            .await?;
    } else {
        info!("Account refresh disabled by user");
    };
    let selected_account =
        account::AccountLaunchInfo::new(config.current_account_uuid, &config.current_account_type)?;

    if config.launch.skip_check_files {
        info!("File checking disabled by user")
    } else {
        let progress = Progress::default();
        {
            let mut status = status.lock().expect("Internal error");
            *status = LaunchEvent::CompleteFiles(progress.clone());
        }
        complete_files(&instance, &minecraft_location, progress, &config.download).await?;
    }

    info!("Generating startup parameters");
    let launch_options = LaunchOptions::new(&config, &instance, selected_account);
    {
        let mut status = status.lock().expect("Internal error");
        *status = LaunchEvent::GenerateScriptlet;
    }
    let version_json_path = minecraft_location.get_version_json(instance.get_version_id()?);
    let raw_version_json = async_fs::read_to_string(version_json_path).await?;
    let resolved_version = resolve_version(
        &Version::from_str(&raw_version_json)?,
        &minecraft_location,
        &[],
    )
    .await?;
    let command_arguments = generate_command_arguments(
        &minecraft_location,
        &instance,
        &launch_options,
        &resolved_version,
    )
    .await?;
    if launch_options
        .account_launch_info
        .yggdrasil_api_root
        .is_some()
    {
        info!("Downloading authlib injector");
        let progress = Progress::default();
        {
            let mut status = status.lock().expect("internal error");
            *status = LaunchEvent::InstallAuthlibInjector(progress.clone());
        }
        install::authlib_injector::install_latest(
            &minecraft_location,
            &resolved_version.id,
            &progress,
        )
        .await?;
    }

    let result = spawn_minecraft_process(command_arguments, launch_options, instance, status).await;
    if let Err(e) = &result {
        error!("Failed to spawn Minecraft process: {e}");
    }
    result
}

fn print_instance_info(instance: &Instance) {
    info!("------------- Instance runtime config -------------");
    info!("-> Minecraft: {}", instance.config.runtime.minecraft);
    match &instance.config.runtime.mod_loader_type {
        Some(x) => info!("-> Mod loader: {x}"),
        None => info!("-> Mod loader: none"),
    };
    match &instance.config.runtime.mod_loader_version {
        Some(x) => info!("-> Mod loader version: {x}"),
        None => info!("-> Mod loader version: none"),
    };
}

/// Spawns the Minecraft process by generating and executing a launch script,
/// customized per operating system and instance configuration.
///
/// # Arguments
/// * `command_arguments` - A list of parsed arguments.
/// * `minecraft_location` - Path to the Minecraft game files.
/// * `launch_options` - Launch customization options (pre/post-execution hooks, wrappers, etc.).
/// * `version_id` - The Minecraft version to launch.
/// * `instance` - The instance metadata and configuration.
///
/// # Behavior
/// * Creates a platform-specific shell script/batch file for launching the game.
/// * Runs the generated script using a subprocess.
/// * Streams stdout to detect key launch indicators and forward logs to the frontend.
/// * Emits `launch_success` event once LWJGL is detected.
/// * Handles cleanup of native libraries after game launch completes.
async fn spawn_minecraft_process(
    command_arguments: Vec<String>,
    launch_options: LaunchOptions,
    instance: Instance,
    status: Arc<Mutex<LaunchEvent>>,
) -> Result<u32> {
    // TODO: 要求 Java 使用高性能显卡
    let minecraft_location = launch_options.minecraft_location.clone();
    let version_id = instance.get_version_id()?;
    let native_root = minecraft_location.get_natives_root(&version_id);
    let instance_root = DATA_LOCATION.get_instance_root(&instance.id);
    let mut commands = String::new();
    if PLATFORM_INFO.os_family == OsFamily::Linux {
        commands.push_str("#!/bin/bash\n\n");
    }
    let comment_prefix = if PLATFORM_INFO.os_family == OsFamily::Windows {
        "::"
    } else {
        "#"
    };
    commands.push_str(&format!(
        "{comment_prefix} This file is automatically generated by Conic Launcher.\n"
    ));
    commands.push_str(&format!(
        "{comment_prefix} NOTE: Don't use this file to launch game.\n\n"
    ));
    commands.push_str(&format!("cd \"{}\"\n", instance_root.to_string_lossy()));
    commands.push_str(&format!("{}\n", launch_options.execute_before_launch));
    if !launch_options.wrap_command.trim().is_empty() {
        commands.push_str(&format!("{} ", launch_options.wrap_command));
    }
    // todo(after java exec): add -Dfile.encoding=encoding.name() and other
    let mut launch_command = "java".to_string();
    for arg in command_arguments.clone() {
        launch_command.push(' ');
        launch_command = format!("{launch_command}{arg}");
    }
    commands.push_str(&launch_command);
    if PLATFORM_INFO.os_family == OsFamily::Windows {
        commands.push_str(&format!("\ndel /F /Q {}\n", native_root.to_string_lossy()))
    } else {
        commands.push_str(&format!("\nrm -rf {}\n", native_root.to_string_lossy()))
    };
    commands.push_str(&format!("{}\n", launch_options.execute_after_launch));
    let script_path = match PLATFORM_INFO.os_family {
        OsFamily::Linux => instance_root.join(".cache").join("launch.sh"),
        OsFamily::Macos => instance_root.join(".cache").join("launch.sh"),
        OsFamily::Windows => instance_root.join(".cache").join("launch.bat"),
    };
    if let Some(script_path_parent) = script_path.parent() {
        std::fs::create_dir_all(script_path_parent)?;
    }
    std::fs::write(&script_path, commands)?;
    info!("The startup script is written to {}", script_path.display());
    let mut minecraft_process = match PLATFORM_INFO.os_family {
        OsFamily::Windows => std::process::Command::new(script_path),
        _ => {
            info!("Running chmod +x {}", script_path.display());
            let mut chmod = Command::new("chmod");
            chmod.args(["+x", script_path.to_string_lossy().to_string().as_ref()]);
            chmod.status()?;
            let mut command = std::process::Command::new("bash");
            command.arg(script_path);
            command
        }
    }
    .stdout(Stdio::piped())
    .spawn()?;
    {
        let mut status = status.lock().expect("Internal error");
        *status = LaunchEvent::WaitForLaunch;
    }
    info!("Spawning minecraft process");
    let out = minecraft_process
        .stdout
        .take()
        .ok_or(Error::TakeMinecraftStdoutFailed)?;
    let mut out = std::io::BufReader::new(out);
    let pid = minecraft_process.id();
    let status_cloned = status.clone();
    thread::spawn(move || {
        let mut buf = String::new();
        while out.read_line(&mut buf).is_ok() {
            if let Ok(Some(_)) = minecraft_process.try_wait() {
                break;
            }
            let lines: Vec<_> = buf.split("\n").collect();
            if let Some(last) = lines.get(lines.len() - 2) {
                trace!("[{pid}] {last}");
                if last.contains("Setting user:") {
                    let mut status = status.lock().expect("Internal error");
                    *status = LaunchEvent::LogSettingUser;
                }
                if last.to_lowercase().contains("lwjgl version") {
                    info!("Found LWJGL version, the game seems to have started successfully.");
                    let mut status = status.lock().expect("Internal error");
                    *status = LaunchEvent::LogLwjglVersion;
                }
                if last.contains("OpenAL initialized") {
                    let mut status = status.lock().expect("Internal error");
                    *status = LaunchEvent::LogOpenALLoaded;
                }
                if last.contains("Created") {
                    let mut status = status.lock().expect("Internal error");
                    *status = LaunchEvent::LogTextureLoaded;
                }
            }
        }
        let output = match minecraft_process.wait_with_output() {
            Ok(output) => output,
            Err(_) => {
                error!("Could not get Minecrafr exit code");
                return;
            }
        };
        if !output.status.success() {
            // TODO: log analysis and remove libraries lock file
            // NOTE: Should use tauri global event here
            // WARN: When failed, frontend should stop all "launching" animation
            error!("Minecraft exits with error code {}", output.status);
        } else {
            info!("Minecraft exits with error code {}", output.status);
        }
    });
    let start = Instant::now();
    while start.elapsed().as_secs() < 60
        && *status_cloned.lock().expect("Internal error") != LaunchEvent::LogTextureLoaded
    {
        async_io::Timer::after(Duration::from_secs(1)).await;
    }
    Ok(pid)
}
